import logging
import os
import sys
from collections import namedtuple

import extra_views
from django.forms import modelform_factory
from django.urls import re_path, reverse

from . import actions
from . import apiviews
from . import settings
from . import utils
from . import views


logger = logging.getLogger("djadmin2")


class ModelAdminBase2(type):
    def __new__(cls, name, bases, attrs):
        new_class = super().__new__(cls, name, bases, attrs)
        view_list = []
        for key, value in attrs.items():
            if isinstance(value, views.AdminView):
                if not value.name:
                    value.name = key
                view_list.append(value)

        view_list.extend(getattr(new_class, "views", []))
        new_class.views = view_list
        return new_class


class ModelAdmin2(metaclass=ModelAdminBase2):
    """
    Adding new ModelAdmin2 attributes:

        Step 1: Add the attribute to this class
        Step 2: Add the attribute to djadmin2.settings.MODEL_ADMIN_ATTRS

        Reasoning:

            Changing values on ModelAdmin2 objects or their attributes from
            within a view results in leaky scoping issues. Therefore, we use
            the immutable_admin_factory to render the ModelAdmin2 class
            practically immutable before passing it to the view. To constrain
            things further (in order to protect ourselves from causing
            hard-to-find security problems), we also restrict which attrs are
            passed to the final ImmutableAdmin object (i.e. a namedtuple).
            This prevents us from easily implementing methods/setters which
            bypass the blocking features of the ImmutableAdmin.
    """

    actions_selection_counter = True
    date_hierarchy = False
    list_display = ("__str__",)
    list_display_links = ()
    list_filter = ()
    list_select_related = False
    list_per_page = 100
    list_max_show_all = 200
    list_editable = ()
    search_fields = ()
    save_as = False
    save_on_top = False
    verbose_name = None
    verbose_name_plural = None
    model_admin_attributes = settings.MODEL_ADMIN_ATTRS
    ordering = False
    save_on_top = False
    save_on_bottom = True

    # Not yet implemented. See #267 and #268
    actions_on_bottom = False
    actions_on_top = True

    search_fields = []

    # Show the fields to be displayed as columns
    # TODO: Confirm that this is what the Django admin uses
    list_fields = []

    # This shows up on the DocumentListView of the Posts
    list_actions = [actions.DeleteSelectedAction]

    # This shows up in the DocumentDetailView of the Posts.
    document_actions = []

    # Shows up on a particular field
    field_actions = {}

    # Defines custom field renderers
    field_renderers = {}

    fields = None
    exclude = None
    fieldsets = None
    form_class = None
    filter_vertical = ()
    filter_horizontal = ()
    radio_fields = {}
    prepopulated_fields = {}
    formfield_overrides = {}
    readonly_fields = ()
    ordering = None

    create_form_class = None
    update_form_class = None

    inlines = []

    #  Views
    index_view = views.AdminView(r"^$", views.ModelListView, name="index")
    create_view = views.AdminView(r"^create/$", views.ModelAddFormView, name="create")
    update_view = views.AdminView(
        r"^(?P<pk>[0-9]+)/$", views.ModelEditFormView, name="update"
    )
    detail_view = views.AdminView(
        r"^(?P<pk>[0-9]+)/update/$", views.ModelDetailView, name="detail"
    )
    delete_view = views.AdminView(
        r"^(?P<pk>[0-9]+)/delete/$", views.ModelDeleteView, name="delete"
    )
    history_view = views.AdminView(
        r"^(?P<pk>[0-9]+)/history/$", views.ModelHistoryView, name="history"
    )
    views = []

    # API configuration
    api_serializer_class = None

    # API Views
    api_list_view = apiviews.ListCreateAPIView
    api_detail_view = apiviews.RetrieveUpdateDestroyAPIView

    def __init__(self, model, admin, name=None, **kwargs):
        self.name = name
        self.model = model
        self.admin = admin
        model_options = utils.model_options(model)
        self.app_label = model_options.app_label
        self.model_name = model_options.object_name.lower()

        if self.name is None:
            self.name = "{}_{}".format(self.app_label, self.model_name)

        if self.verbose_name is None:
            self.verbose_name = model_options.verbose_name
        if self.verbose_name_plural is None:
            self.verbose_name_plural = model_options.verbose_name_plural

    def get_default_view_kwargs(self):
        return {
            "app_label": self.app_label,
            "model": self.model,
            "model_name": self.model_name,
            "model_admin": immutable_admin_factory(self),
        }

    def get_index_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update(
            {
                "paginate_by": self.list_per_page,
            }
        )
        return kwargs

    def get_default_api_view_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update(
            {
                "serializer_class": self.api_serializer_class,
            }
        )
        return kwargs

    def get_prefixed_view_name(self, view_name):
        return "{}_{}".format(self.name, view_name)

    def get_create_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        kwargs.update(
            {
                "inlines": self.inlines,
                "form_class": (
                    self.create_form_class
                    if self.create_form_class
                    else self.form_class
                ),
            }
        )
        return kwargs

    def get_update_kwargs(self):
        kwargs = self.get_default_view_kwargs()
        form_class = (
            self.update_form_class if self.update_form_class else self.form_class
        )
        if form_class is None:
            form_class = modelform_factory(self.model, fields="__all__")
        kwargs.update(
            {
                "inlines": self.inlines,
                "form_class": form_class,
            }
        )
        return kwargs

    def get_index_url(self):
        return reverse("admin2:{}".format(self.get_prefixed_view_name("index")))

    def get_api_list_kwargs(self):
        kwargs = self.get_default_api_view_kwargs()
        kwargs.update(
            {
                "queryset": self.model.objects.all(),
                # 'paginate_by': self.list_per_page,
            }
        )
        return kwargs

    def get_api_detail_kwargs(self):
        return self.get_default_api_view_kwargs()

    def get_urls(self):
        pattern_list = []
        for admin_view in self.views:
            admin_view.model_admin = self
            get_kwargs = getattr(self, "get_%s_kwargs" % admin_view.name, None)
            if not get_kwargs:
                get_kwargs = admin_view.get_view_kwargs
            try:
                view_instance = admin_view.view.as_view(**get_kwargs())
            except Exception as e:
                trace = sys.exc_info()[2]
                new_exception = TypeError(
                    'Cannot instantiate admin view "{}.{}". '
                    "The error that got raised was: {}".format(
                        self.__class__.__name__, admin_view.name, e
                    )
                )
                try:
                    raise new_exception.with_traceback(trace)
                except AttributeError:
                    raise (new_exception, None, trace)

            pattern_list.append(
                re_path(
                    admin_view.url,
                    view=view_instance,
                    name=self.get_prefixed_view_name(admin_view.name),
                )
            )
        return pattern_list

    def get_api_urls(self):
        return [
            re_path(
                r"^$",
                view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
                name=self.get_prefixed_view_name("api_list"),
            ),
            re_path(
                r"^(?P<pk>[0-9]+)/$",
                view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()),
                name=self.get_prefixed_view_name("api_detail"),
            ),
        ]

    @property
    def urls(self):
        # We set the application and instance namespace here
        return self.get_urls(), None, None

    @property
    def api_urls(self):
        return self.get_api_urls(), None, None

    def get_list_actions(self):
        actions_dict = {}

        for cls in type(self).mro()[::-1]:
            class_actions = getattr(cls, "list_actions", [])
            for action in class_actions:
                actions_dict[action.__name__] = {
                    "name": action.__name__,
                    "description": actions.get_description(action),
                    "action_callable": action,
                }
        return actions_dict

    def get_ordering(self, request):
        return self.ordering


class Admin2Inline(extra_views.InlineFormSetFactory):
    """
    A simple extension of django-extra-view's InlineFormSet that
    adds some useful functionality.
    """

    template = None
    fields = "__all__"

    def construct_formset(self):
        """
        Overrides construct_formset to attach the model class as
        an attribute of the returned formset instance.
        """
        formset = super().construct_formset()
        formset.model = self.inline_model
        formset.template = self.template
        return formset


class Admin2TabularInline(Admin2Inline):
    template = os.path.join(
        settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/tabular.html"
    )


class Admin2StackedInline(Admin2Inline):
    template = os.path.join(
        settings.ADMIN2_THEME_DIRECTORY, "edit_inlines/stacked.html"
    )


def immutable_admin_factory(model_admin):
    """
    Provide an ImmutableAdmin to make it harder for developers to
    dig themselves into holes.
    See https://github.com/twoscoops/django-admin2/issues/99
    Frozen class implementation as namedtuple suggested by Audrey Roy

    Note: This won't stop developers from saving mutable objects to
    the result, but hopefully developers attempting that
    'workaround/hack' will read our documentation.
    """
    ImmutableAdmin = namedtuple("ImmutableAdmin", model_admin.model_admin_attributes)
    return ImmutableAdmin(
        *[getattr(model_admin, x) for x in model_admin.model_admin_attributes]
    )
